More With Microsoft Agent: Let There Be Tools!
by François Gaillard
I found these articles (Microsoft Agent article , More on Microsoft Agent) on Microsoft Agent very interesting, and had some fun playing with the example. Then I had some questions...
More than one character:
A single Agent can load many (I did not see the limit, it may be somewhere in the docs) different characters with two limitations:
Agent.Characters.Load('Default', UnAssigned);Where ‘Default’ is (as usual) the name you want to give to that loaded character. You then can call it by:
Agent.Characters.Character('Default'...Of course, you can call it «genie» if you want, even if it is Merlin.
Lot of characters:
To load many characters, you just call load more than once:
Agent.Characters.Load('Genie', 'Genie.acs');You will get an EOleException ‘File not found’ if the corresponding *.ACS file is not installed. Remember that:
Agent.Characters.Load('Merlin', 'Merlin.acs');
Agent.Characters.Load('Peedy', 'Peedy.acs');
Agent.Characters.Load('Robby', 'Robby.acs');
Agent.Characters.Load('Genie', 'Merlin.acs');is correct, though a bit dirty.
Agent.Characters.Load('Merlin', 'Genie.acs');
Speeding text writing into the balloon
The speed at which an Agent writes into the balloon depends of the
text-to-speech settings, and cannot be changed by program! It is a per
User setting, and developpers are just allowed to read the speed value.
So, if you are like a lot of developpers and prefer to set things yourself
without interference of the users, there is a trick, done by the MaxSpeedSpeach
procedure.
We’ll simulate the user opening the text-to-speech dialog:
anAgent.PropertySheet.Visible:=true;Then find the window handle of the speed slider, and the appropriate message:
anAgent.PropertySheet.Page := 'Output';
sendMessage(w,TBM_SETPOS,1,100);Then find the window handle of the OK button and send a click:
sendMessage(w,BM_CLICK, 0, 0);The trick is of course to «find the window handle». Winsight gave us that the parent form is a «#32770» class window, and the slider is a «msctls_trackbar32» class window. The button is of course a «Button» class window, and we need also it’s title «OK»to find it. We use an EnumProc callback function where we pass a reference to an array of 3 pointers, corresponding respectively to the classname, title (in) and the handle (out) of the searched window. I do not know if these class names are worldwhile the same… so better check it!
Dynamically listing the available Animations
If you look at the Character interface you’ll find a AnimationNames
property which has an enumeration. Using an IEnumVariant interface to loop
with it’s next method gave the list of the available animations.
This is the purpose of the GetAnimationList procedure. You give it
a Character argument, and a TStrings where you’ll get back the list of
all Animations names usable as argument to the play method of that Character.
Now, let’s build a comfortable program. I have freely modified the AgentDemo program (revised by Nikolai Botev) to add a sort of «Animation browser».
First, I wanted to have the main characters available: Genie, Merlin, Peedy and Robby. So I modified the FormCreate to load all these 4 characters. I dropped a RadioGroup to select which of them is to be under control. I draw a TlistBox (sorted) to handle the Animations List. With a onclick event to launch the clicked animation. And a button to retrieve the animations list of the selected character. I did not want to build it automatically when the character selection changes because I wanted to compare quickly the animations amongst the characters. And so you can test what happens when you play a wrong animation: for instance Merlin has less idle_x ones than Genie. I should have put the MaxSpeedSpeach routine at the FormCreate level, but I wanted to see that «property sheet» blinking at each time I rebuild the list. Sort of a little revenge…
Code
( the complete project is zipped in AgentDemo2.zip)
procedure TfrmMain.FormShow(Sender: TObject);
begin
// Agent.Characters.Load('Default', UnAssigned);
try
Agent.Characters.Load('Genie', 'Genie.acs');
except
end;
try
Agent.Characters.Load('Merlin', 'Merlin.acs');
except
end;
try
Agent.Characters.Load('Peedy', 'Peedy.acs');
except
end;
try
Agent.Characters.Load('Robby', 'Robby.acs');
except
end;
end;
procedure TfrmMain.btnGetAnimClick(Sender: TObject);
begin
MaxSpeedSpeach(Agent);
// GetAnimationList(Agent.Characters.Character('default'),ListBox1.items);
with rdgCharacters do
if ItemIndex>-1 then
GetAnimationList(Agent.Characters.Character(Items[ItemIndex]),ListBox1.items);
end;procedure TfrmMain.ListBox1Click(Sender: TObject);
begin
with Sender as TListBox do
if ItemIndex>-1 then
with Agent.Characters.Character(rdgCharacters.Items[rdgCharacters.ItemIndex]) do begin
show(True); // ensure it is visible
stopAll(''); // stop previous animations
play(items[itemindex]); // play the choosen one
end;
end;
{ Get a list of all animations (for the play method) existing for a character}
procedure GetAnimationList(IAChar:IAgentCtlCharacterEx;Ts:TStrings);
{ IAChar : the character like Agent.Characters.Character('Genie')
Ts : the TStrings to be filled with the list of his available animations}
const
{ avoid a use clause on OLE2 }
IID_IEnumVariant: TGUID = (
D1:$00020404;D2:$0000;D3:$0000;D4:($C0,$00,$00,$00,$00,$00,$00,$46));
var
pEnum:IEnumVARIANT ;
vAnimName:VARIANT ;
dwRetrieved:DWORD ;
hRes: HResult;
begin
hRes:=IAChar.AnimationNames.enum.QueryInterface(IID_IEnumVARIANT, pEnum);
if S_Ok=hRes then begin
if Ts.count<>0 then begin
IAChar.show(True); //0=false, plays animation if any; 1=true, quicker show without anim
IAChar.Speak('I will clear the List!','');
// IAChar.Think('I will clear the List!');
ts.Clear;
end;
while (TRUE) do begin
hRes:=pEnum.Next(1, vAnimName, @dwRetrieved);
if S_OK<>hRes then
break;
// vAnimName is the animation Name
ts.add(vAnimName);
//VariantClear(&vAnimName);
end;
//pEnum.free();
end;
end;{ set the speaking speed at it's highest }
procedure MaxSpeedSpeach(anAgent:TAgent);
type
arp= array[0..2] of Pointer;
const
s1:PChar='msctls_trackbar32';
s2:PChar='Button';
s3:PChar='OK';
var
h,w:THandle;
p: arp;{ the callback function for EnumChildWindows }
function EnumProc(W:THandle; LP:LParam):Wordbool; stdcall;
{ we pass the address of an array of 3 pointers as LParam
INPUTs : pointers to classname and title of the desired window
OUTPUT : Pointer to the found window handle;
type
arp= array[0..2] of Pointer; }
var
s:PChar;
begin
Result:=true;
s:=strAlloc(255);
try
if GetClassName(w,s,254)>0 then begin
if strIComp(s,PChar(arp(Pointer(LP)^)[0]^))=0 then begin
{$IFDEF DEBUG}
ShowMessage('ok pchar1');
{$ENDIF}
if arp(Pointer(LP)^)[1]<>nil then begin
// look for caption
if GetWindowText(w,s,100)=0 then begin
exit;
end else begin
if strIComp(s,PChar(arp(Pointer(LP)^)[1]^))<>0 then begin
exit;
end;
end;
end;
// we have found the desired window
arp(Pointer(LP)^)[2]:=@w;
Result:=false; // to stop enumeration
end;
{$IFDEF DEBUG}
end else begin
ShowMessage(IntToStr(GetLastError));
ShowMessage(s);
{$ENDIF}
end;
finally
strDispose(s);
end;
end; { EnumProc }begin { MaxSpeedSpeach }
// Open the agent property dialog
anAgent.PropertySheet.Visible:=true;
anAgent.PropertySheet.Page := 'Output';// find the Parent window containing the speed slider
h:=findWindow('#32770',nil);
{$IFDEF DEBUG}
ShowMessage(IntToStr(h));
{$ENDIF}// find the slider : 'msctls_trackbar32','Slider1'
p[0]:=@s1;
p[1]:=nil;
EnumChildWindows(h,@EnumProc,Longint(@p));
w:=PLongInt(p[2])^;
// found the slider, send it to the maximum speed
sendMessage(w,TBM_SETPOS,1,100);
{$IFDEF DEBUG}
ShowMessage(IntToStr(w));
{$ENDIF}// find the OK Button 'Button','OK'
p[0]:=@s2;
p[1]:=@s3;
EnumChildWindows(h,@EnumProc,Longint(@p));
w:=PLongInt(p[2])^;
// found the OK Button; send a click to Close dialog
sendMessage(w,BM_CLICK, 0, 0);
{$IFDEF DEBUG}
ShowMessage(IntToStr(w));
{$ENDIF}
end; { MaxSpeedSpeach }